package controller

import (
	"gitee.com/kristas/booting-go/framework/common/util/lang"
	"gitee.com/kristas/booting-go/framework/common/util/reflectx"
	"gitee.com/kristas/booting-go/framework/web"
	"gitee.com/kristas/booting-go/framework/web/resp"
	"gitee.com/kristas/booting-go/framework/web/web_filter"
	"github.com/go-playground/validator/v10"
	"net/http"
	"reflect"
)

type ApiInfo struct {
	httpMethod string
	url        string
	desc       string
	params     []web.Param
	methodName string
	group      string
	field      reflect.StructField
}

func NewApiInfo(group string, field reflect.StructField, svm *ServiceMethod) web.ApiInfo {
	var api = &ApiInfo{
		desc:       getDescription(field),
		group:      group,
		methodName: lang.NewString(field.Name).FirstLetterUpper().String(),
		field:      field,
	}
	api.httpMethod, api.url = findHttpMethodAndUrl(field)
	if svm == nil {
		api.params = buildParams(field.Tag.Get("param"), nil)
	} else {
		api.params = buildParams(field.Tag.Get("param"), svm.GetMethod().Type)
	}
	return api
}

func (a *ApiInfo) GetHttpMethod() string {
	return a.httpMethod
}

func (a *ApiInfo) GetURL() string {
	return a.group + a.url
}

func (a *ApiInfo) GetDescription() string {
	return a.desc
}

func (a *ApiInfo) GetParams() []web.Param {
	return a.params
}

func (a *ApiInfo) GetField() reflect.StructField {
	return a.field
}

func (a *ApiInfo) GetMethodName() string {
	return a.methodName
}

func (a *ApiInfo) GetMeta(tag string) string {
	return a.GetField().Tag.Get(tag)
}

type Param struct {
	name        string
	source      string
	specifyType string
	typ         reflect.Type
}

func (p *Param) GetName() string {
	return p.name
}

func (p *Param) GetSource() string {
	return p.source
}

func (p *Param) GetSpecifyType() string {
	return p.specifyType
}

func (p *Param) GetType() reflect.Type {
	return p.typ
}

func (p *Param) SetType(t reflect.Type) {
	p.typ = t
}

type ServiceMethod struct {
	Method *reflect.Method
	Value  *reflect.Value
}

func NewServiceMethod(method *reflect.Method, value *reflect.Value) *ServiceMethod {
	return &ServiceMethod{Method: method, Value: value}
}

func (s *ServiceMethod) GetMethod() *reflect.Method {
	return s.Method
}

func (s *ServiceMethod) GetValue() *reflect.Value {
	return s.Value
}

type Endpoint struct {
	web.ApiInfo
	web.ServiceMethod
}

func NewEndpoint(controller web.Controller) []web.Endpoint {
	endpoints := make([]web.Endpoint, 0)
	RestApiExtract(controller, func(apiInfo web.ApiInfo, svm web.ServiceMethod) {
		endpoints = append(endpoints, &Endpoint{
			ApiInfo:       apiInfo,
			ServiceMethod: svm,
		})
	})
	return endpoints
}

func (e *Endpoint) GetApiInfo() web.ApiInfo {
	return e.ApiInfo
}

func (e *Endpoint) GetServiceMethod() web.ServiceMethod {
	return e.ServiceMethod
}

func (e *Endpoint) Handler(adapter web.ParamAdapter, handler func(result interface{}) error) {
	mapper := newParamMapper(adapter)
	writer := mapper.GetResponseWriter()
	result, err := dispatcherService(e.ServiceMethod, e.ApiInfo, mapper)
	if err != nil {
		handleErr(writer, err)
	}
	if result != nil {
		err := resolveResponse(writer, result, handler)
		if err != nil {
			handleErr(writer, err)
		}
	}
}

func dispatcherService(svm web.ServiceMethod, apiInfo web.ApiInfo, mapper ParamMapper) (interface{}, error) {
	var result interface{}
	// start filter chain
	err := web_filter.NewFilterChain(mapper.GetResponseWriter(), mapper.GetRequest(), func() error {
		var err error
		result, err = callFunc(svm, apiInfo.GetParams(), mapper)
		return err
	})
	if err != nil {
		return nil, err
	}
	return result, nil
}

func resolveResponse(w http.ResponseWriter, result interface{}, handler func(result interface{}) error) error {
	// handle built-in resp.Response result
	if res, ok := result.(resp.Response); ok {
		res.Write(w)
		return nil
	}
	// do web framework handler
	return handler(result)
}

func handleErr(w http.ResponseWriter, err error) {
	if respErr, ok := err.(resp.Response); ok {
		respErr.Write(w)
	} else {
		resp.ErrInternalError(err).Write(w)
	}
}

func callFunc(svm web.ServiceMethod, params []web.Param, mapper ParamMapper) (interface{}, error) {
	paramValues, err := prepareParamValues(svm.GetMethod(), params, mapper)
	if err != nil {
		return nil, err
	}
	err = validateParamValues(paramValues)
	if err != nil {
		return nil, err
	}
	resultValue := svm.GetValue().Call(reflectx.Interfaces2Values(paramValues))
	results := reflectx.Values2Interfaces(resultValue)
	switch len(results) {
	case 0:
		return nil, nil
	case 2:
		if err, ok := results[1].(error); ok {
			return nil, err
		}
		return results[0], nil
	default:
		return results[0], nil
	}
}

func prepareParamValues(method *reflect.Method, params []web.Param, mapper ParamMapper) ([]interface{}, error) {
	m := method.Type
	var values []interface{}
	for i := 0; i < m.NumIn()-1; i++ {
		o, err := getValue(params[i], mapper)
		if err != nil {
			return nil, err
		}
		values = append(values, o)
	}
	return values, nil
}

func validateParamValues(params []interface{}) error {
	for i := range params {
		param := params[i]
		err := validateParam(param)
		if err != nil {
			return err
		}
	}
	return nil
}

func validateParam(param interface{}) error {
	switch reflect.TypeOf(param).Kind() {
	case reflect.Struct:
		validate := validator.New()
		err := validate.Struct(param)
		if err != nil {
			return err
		}
	case reflect.Ptr:
		return validateParam(reflect.ValueOf(param).Elem().Interface())
	}
	return nil
}
